1 - Portals

createPortal 是 React DOM 提供的一个方法,它允许你将组件渲染到 DOM 树结构中的不同位置,而不是作为其父组件的子节点渲染。

在 React 中,组件通常会渲染到其父组件 DOM 节点内部。然而,createPortal 提供了一种“传送门”机制,让你可以将一个 React 元素渲染到 DOM 树中的任何现有节点上,即使这个节点不在该 React 组件的父级 DOM 结构中。

createPortal 的语法

createPortal 接收两个参数:

  1. child:任何可渲染的 React 子元素,例如 JSX 元素、字符串、片段(Fragment)等。
  2. container:一个 DOM 元素。这个元素是你希望 Portal 渲染到的目标 DOM 节点。

核心原理

虽然 Portal 渲染的 DOM 节点在视觉上位于 DOM 树的其他位置,但在 React 内部,它仍然是定义它的组件的子组件。这意味着:

基本使用

创建一个AlertMessage组件,并使用它。

需要在public/index.html里面,创建一个单独的挂载节点,避免挂载到body上,造成app里面页面的重绘重排。

image-20251209133409724

效果:

注意:

详情可以看react实践这个文档里面。

2 - Passing Refs to Components

我们会使用useRef来引用DOM元素或react组件,当我们直接在组件里面赋值给DOM元素的时候,是OK的。

效果是OK的:

问题是:怎么将ref传递给子组件呢?

在react19中,可以直接将ref传递给子组件了。react19一下的版本,需要用到forwardRef,下节课会讲到。

效果还是OK的。

3 - forwardRef

forwardRef 是 React 提供的高阶组件(Higher-Order Component, HOC)函数,它允许父组件将 ref 属性作为普通的参数向下传递(转发)给其子组件内部的 DOM 节点或另一个 React 组件。

只需要将子组件使用forwardRef包裹起来就行了,非常简单。

1、定义一个CustomInput组件

2、使用这个组件

使用OK:

需要注意的是,在使用forwardRef时,里面的组件函数的参数,ref参数一定要放到所有参数的最后一个,这样才能单独指定。

4 - Modal Introduction

这节课讲解了一下在html+css+js代码中,怎么实现modal。参考:https://github.com/WebDevSimplified/React-Simplified-Advanced-Projects/tree/main/01-modal/before

方式一:使用div,让display在none和block之间切换来显示/隐藏。

方式二:使用dialog元素,通过操作它的showModal()和close()方法,可以显示/隐藏。

5 - Modal Walkthrough

这节课讲解怎么在react中实现modal。还是可以有两种方式,但是必须使用createPortal方法。

1、使用div来创建Modal

①编写样式

就是利用.modal-overlay的display属性来显示/隐藏。

②编写组件

③使用组件

可以看到,效果OK:

④为modal添加键盘的escape键事件,当按这个键的时候,也可以关闭弹窗

可以看到,鼠标没有动,按下escape键就关闭modal了。

2、使用dialog元素来实现Modal

dialog元素,通过操作它的showModal()和close()方法,可以显示/隐藏。

①编写组件

②引用组件

效果OK:

 

③有一个问题

dialog元素默认支持使用escape键来关闭,但当使用escape键关闭之后,就再也不能打开了。这是因为此时的isDialogModalOpen仍然为true,所以就会默认此种状态。

所以要在dialog上加一个useEffect来监听close事件,当close事件发生时,执行onClose方法。

可以看到,鼠标没有移动,使用escape键关闭dialog之后,可以正常打开。

6 - Error Boundaries

react16之后,提供了ErrorBoundary功能。错误边界 是一个 React 组件,它能够捕获其子组件树中任意位置发生的 JavaScript 错误记录这些错误,并显示一个 备用 (fallback) UI,而不是让整个应用崩溃。

注意:是里面的子组件如果有JS错误,就会造成整个应用崩溃。所以ErrorBoundary组件很重要。

创建

ErrorBoundary是react中唯一一个必须使用class component来创建的组件。并且需要定义以下两个或其中一个特殊的生命周期方法:

  1. static getDerivedStateFromError(error)

这是一个 静态方法,用于在子组件抛出错误后渲染备用 UI

  1. componentDidCatch(error, errorInfo)

这是一个非静态方法,用于在子组件抛出错误后执行副作用

组件示例如下,这个组件的功能已经完成了。剩下需要考虑的就是在哪里使用这个组件,并且做好fallback页面。

哪些地方需要用到ErrorBoundary

① 一定要在App外层包裹一个ErrorBoundary,防止任何意想不到的错误的发生。作为最后的防线,捕获任何未被子级错误边界处理的全局性、渲染阶段的错误。

Fallback UI: 在这里,Fallback UI 可能是一个通用的错误页面,包含一个“刷新”或“回到首页”的按钮,因为此时应用的大部分内容可能已崩溃。

② 路由/页面级 (Route/Page-Level),裹路由配置中每个页面的主要组件,确保一个页面的崩溃不会影响到其他页面或应用的导航结构。

③ 独立且关键的 UI 模块,隔离那些拥有复杂逻辑或依赖外部数据、但其崩溃不应影响整个页面的组件。

 

ErrorBoundary只在核心位置使用,不要过度使用;并且Fallback界面要提供有用的信息及恢复操作;要收集错误日志,利用 componentDidCatch 方法将错误信息发送到外部日志服务(如 Sentry, LogRocket, Splunk 等),这是错误边界的关键价值之一。

错误边界不能捕获什么

错误边界只能捕获其子组件树中发生的错误。它不能捕获以下类型的错误:

  1. 事件处理器内部的错误 (Errors inside event handlers): 事件处理器是在 React 的同步事件流之外运行的。如果你需要处理这些错误,请使用标准的 JavaScript try/catch 语句。
  2. 异步代码内部的错误 (Errors inside asynchronous code): 例如 setTimeoutPromise.then() 中的错误。
  3. 错误边界组件自身的错误: 如果 ErrorBoundary 组件自身在其 render 方法中抛出错误,它将无法捕获自己。这个错误就一定要避免。
  4. 服务端渲染 (SSR) 时的错误: 错误边界仅在客户端渲染(Client-Side Rendering)期间捕获错误。

7 - Advanced Key Users

这节课学习一个重要概念,how react handle state preservation between different rerenders.

先看一个案例:

创建子组件Counter:

在父组件中使用它:

注意看,当Switch切换时,Couter状态会保留吗?

可以看到Counter的状态保留了。

疑问:为什么会出现这样的情况呢?我明明使用了两个Counter组件,为什么它们的状态是共享的?

原因:在react中,当它判断组件的状态时,实际上react并不会关注是哪个组件,它关注的是组件在组件树中的位置。

React 默认通过 组件在 JSX 结构中的位置 来确定组件的身份,而不是组件的类型。你在 changeDogs ? <Counter /> : <Counter /> 写的是两个位置完全一样的 <Counter />,React 看到类型相同、位置相同、没有 key,就认为“这是同一个组件只是文本变了”,所以它根本没有卸载旧的 Counter,而是直接复用了它的 DOM 和 state!

解决办法:在使用Counter组件时,加上key属性。就是显式地告诉 React,在 changeDogs 状态切换时,这两个 <Counter /> 实际上是两个不同的组件,即使它们在代码中的位置看起来相似。

这样就正常了:

这是react中经典的组件身份和状态保留问题,如果在写项目时,遇到了这种现象,要想到怎么解决。

8 - Capture Event Listeners

在react中,一般事件传播的过程都是事件冒泡。

事件冒泡(Event Bubbling)是一种 DOM 事件传播机制,当一个元素上的事件被触发时,该事件会从触发元素开始,逐层向父元素、祖父元素,一直冒泡(传递)到文档根节点 (document) 的过程,就像水底的泡泡上升一样,它允许父元素统一处理子孙元素发生的事件。

像最经典的onClick事件,就会引起事件冒泡。

但是有时候我们想使用事件捕获,应该怎么做呢?需要在事件捕获的元素上面,都使用事件捕获监听器。

要在 React 中注册捕获事件监听器,你只需在标准的事件属性名称后面添加 Capture 后缀即可:

冒泡阶段监听器 (默认)捕获阶段监听器
onClickonClickCapture
onMouseDownonMouseDownCapture
onKeyDownonKeyDownCapture
onChangeonChangeCapture

使用场景:

如果你需要在事件到达目标元素之前就阻止它的传播,你可以使用捕获监听器。

当你需要实现一个功能,比如点击模态框(Modal)外部的任何地方来关闭它时,全局捕获监听器很有用。

9 - Date Picker Introduction

https://github.com/WebDevSimplified/React-Simplified-Advanced-Projects/tree/main/02-date-picker/before,这里提供了老师通过html+js+css实现的DatePicker功能。

10 - Date Picker Walkthrough

不使用Portal来实现DatePicker,因为date的选择框是紧跟着DatePicker按钮的,所以放在一起。

1、安装date-fns库,用来处理日期。

2、将第九节课老师定义好的样式代码copy下来,然后将DatePicker的HTML代码直接copy到组件中,可以看到样式已经出来了,交互功能还没有:

image-20251209192044395

3、将modal相关的代码提取出来,添加显示/隐藏功能

做到这一步就很不错了,如果要我来做,我会感觉到无从下手,所以这一步是怎么实现的,一定要搞清楚。

效果:

 

4、实现功能

这一步实现了下面的功能:

完整代码如下:

引入使用:

效果:

11 - Head Tags

如何更改某个页面的meta和title信息?在使用react router的时候,可以这样修改:https://reactrouter.com/start/framework/route-module#meta

image-20251209202254433

直接添加title和meta元素。

12 - useLayoutEffect

这节课来学习useLayoutEffect。

useLayoutEffect 是一个与 useEffect 签名(函数签名和用法)完全相同的 Hook,但它在 React 的渲染流程中执行的时机是不同的。

useLayoutEffect 主要用于执行那些需要在浏览器进行下一次绘制(paint)之前同步发生的 DOM 操作,例如测量布局、滚动位置或强制同步更新 DOM。

下面用一个例子来说明为什么要使用useLayoutEffect。

可以看到,画面有明显的位移痕迹。

这时候就可以换为useLayoutEffect,它与useEffect的用法完全相同,只是执行机制不同。当看到页面闪动情况时,就要想到它。

image-20251209205048323

可以看到没有闪烁的情况:

那二者的区别是什么呢?就是在react里面执行的时机:

官方和社区的推荐用法(2025 年最新共识)

场景推荐用哪个原因
发网络请求、埋点、setTimeout、订阅事件useEffect不需要同步 DOM,异步执行性能更好
读取 DOM 尺寸(getBoundingClientRect)useLayoutEffect必须在绘制前拿到准确值,否则拿到的是旧值
根据 DOM 尺寸设置初始状态(弹窗居中、图表 resize)useLayoutEffect防止出现「先偏再居中」的闪烁
做动画、过渡的初始位置设置useLayoutEffect避免出现「先闪到错误位置再动画」的 bug
第三方库要求在 DOM 更新后立即操作(比如 echarts.resize)useLayoutEffect确保拿到最新布局
99% 的普通场景useEffect性能更好,不阻塞绘制

13 - useDebugValue

useDebugValue 是一个 React Hook,它主要的作用是帮助开发者在 React DevTools(React 开发者工具)中显示自定义 Hook 的调试信息(debug value)。

不会影响你的组件的任何行为、渲染流程或性能。它纯粹是一个调试工具

当你使用 useStateuseReducer 时,React DevTools 会自动显示状态的当前值。但是,当你创建自己的自定义 Hook 时,DevTools 只会显示你的 Hook 的名称,而不知道它内部管理了什么状态。

useDebugValue 允许你为自定义 Hook 提供一个清晰、可读的标签,让你能快速检查 Hook 的内部状态。

在发生产时,useDebugValue相关代码都要删除,因为会增加不必要的计算开销。

14 - useId

useId 是 React 18 引入的一个 Hook,它的主要作用是生成稳定且唯一的 ID 字符串,用于将客户端组件与服务器端生成的 HTML 元素进行关联。

这个 Hook 解决了在服务端渲染(SSR)和客户端水合(Hydration)过程中,ID 匹配可访问性(Accessibility) 的关键问题。

15 - How to debug react apps like a senior developer

StrictMode logging tip

在开发过程中,一般都会启动StrictMode,但是控制台输出两次确实很迷惑人,所以可以使用react dev tools的一个选项,将输出控制为一次。

image-20251210112614550

可以看到,只输出了一次。

react dev tools调试

Component面板

真的不需要每次在console.log里面输出,甚至在UI里面输出来调试。直接在Component面板里面查看结果会更好。里面有很多信息。

image-20251210113711680

使用Component面板里面的元素选择器,会提供更多、更准确的信息,而且速度快多了。

image-20251210114531237

当使用Suspense包裹住某个组件时,如果你想测试loading的效果,那么可以使用这个工具:

image-20251210115102913

这个工具会暂停选中的组件的渲染,让你查看loading效果。

有时候你想查看某个组件报错后会发生什么,或者测试你的ErrorBoundary做好没有,可以使用这个工具,它会强制使选中的组件进入错误状态。

image-20251210115320295

Profile面板

这是一个非常强大的工具,它可以帮助你理解组件的渲染性能,找出应用中的渲染瓶颈。

image-20251210120434666

操作很简单,就是点击开始按钮,然后在页面上操作,操作完成后关闭,就会显示分析结果。

可以看到,很详细的给出了为什么组件rerender的原因,对于分析很有帮助。

16 - Activity组件

image-20251210133557659

react19新增了Activity组件,可以显示/隐藏里面的子组件,并且保留子组件的UI和内部状态。

一般我们是这样显示/隐藏子组件的:{isVisible && <Counter />},因为会在react组件树中创建/销毁子组件,所以子组件的状态会丢失。

以前的做法

可以看到,当切换显示子组件的时候,子组件的状态都丢失了。

Activity基本使用

只需要将父组件的代码这样改就行了:

可以看到,子组件的状态保留了。

其它作用

预渲染即将可见的内容

隐藏内容会以低优先级渲染,适合预加载数据/代码。

When an Activity boundary is hidden during its initial render, its children won’t be visible on the page — but they will still be rendered, albeit at a lower priority than the visible content, and without mounting their Effects.

This pre-rendering allows the children to load any code or data they need ahead of time, so that later, when the Activity boundary becomes visible, the children can appear faster with reduced loading times.

这个例子中,Posts组件刚开始是隐藏的,但是由于使用在Activity组件中,它的可见内容会以低优先级的pre-render。

加速页面加载时的交互

在 SSR/SSG 中,<Activity>支持选择性水合(Selective Hydration),让可见部分先交互。比如说在nextjs的server components就可以使用Activity来包裹。优化性能。

17 - How To Use CSS Dev Tools Like a Senior Developer

还是使用CHROME浏览器的调试工具,然后就是elements面板里面的样式。

另外,还可以使用ctrl+shift+p,输入rendering,打开这个面板,可以选择一些模拟动作:

image-20251210152850580

输入animation,可以查看页面的动画:

image-20251210152949020

输入CSS overview,可以查看整个网站的样式总览:

image-20251210153043458